namespace EyeSoft.Wpf.Facilities
{
	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.ComponentModel;
	using System.Linq;
	using System.Linq.Expressions;

	using EyeSoft.Messanging;
	using EyeSoft.Reflection;
	using EyeSoft.Validation;

	public abstract class ViewModel :
		INotifyPropertyChanged,
		INotifyPropertyChanging,
		IDataErrorInfo,
		IDisposable
	{
		private readonly ObservableCollection<Exception> errorCollection =
			new ObservableCollection<Exception>();

		private readonly PropertiesChanges changedActions = new PropertiesChanges();

		private readonly PropertiesChanges changingActions = new PropertiesChanges();

		private readonly DependentProperties dependentProperties;

		private readonly PropertiesValues propertiesValues;

		private bool disposed;

		protected ViewModel()
		{
			ErrorCollection = new ReadOnlyObservableCollection<Exception>(errorCollection);
			MessageBroker.Register<ExceptionMessage<object, Exception>>(this, HandleError);

			dependentProperties = new DependentProperties(OnPropertyChanged);
			propertiesValues = new PropertiesValues(OnPropertyChanging, OnPropertyChanged);
		}

		public event PropertyChangedEventHandler PropertyChanged;

		public event PropertyChangingEventHandler PropertyChanging;

		public ReadOnlyObservableCollection<Exception> ErrorCollection
		{
			get; private set;
		}

		public bool IsPropertyChangedSuspended { get; private set; }

		public int Changes
		{
			get { return propertiesValues.Changes; }
		}

		public bool Changed
		{
			get { return propertiesValues.Changes > 0; }
		}

		public virtual bool IsValid
		{
			get { return !Validate().Any(); }
		}

		public string Error
		{
			get
			{
				var error = GetError(Validate);
				return error;
			}
		}

		public string this[string propertyName]
		{
			get
			{
				var error = GetError(() => Validate(propertyName));
				return error;
			}
		}

		public void Dispose()
		{
			Dispose(true);

			GC.SuppressFinalize(this);
		}

		public void ClearErrors()
		{
			errorCollection.Clear();
		}

		public virtual bool CanClose()
		{
			return IsValid;
		}

		protected void ResumePropertyChanged()
		{
			IsPropertyChangedSuspended = false;
		}

		protected void SuspendPropertyChanged()
		{
			IsPropertyChangedSuspended = true;
		}

		protected virtual IEnumerable<ValidationError> Validate()
		{
			return new DefaultValidator().Validate(this);
		}

		protected virtual IEnumerable<ValidationError> Validate(string propertyName)
		{
			return Validate().Where(x => x.PropertyName == propertyName);
		}

		#region property get/set
		protected object GetPropertyValue(string propertyName)
		{
			return propertiesValues.GetValue(propertyName);
		}

		protected T GetPropertyValue<T>(string propertyName)
		{
			return (T)GetPropertyValue(propertyName);
		}

		protected T GetProperty<T>()
		{
			return propertiesValues.GetProperty<T>();
		}

		protected void SetProperty<T>(T value, bool suspendPropertyChanged = false)
		{
			var propertyChangedSuspended = suspendPropertyChanged || IsPropertyChangedSuspended;

			if (propertyChangedSuspended)
			{
				SuspendPropertyChanged();
			}

			propertiesValues.SetProperty(value, propertyChangedSuspended);
			ResumePropertyChanged();
		}
		#endregion

		#region Dependent properties

		protected DependentProperty Property(Expression<Func<object>> propertyExpression)
		{
			return dependentProperties.Register(propertyExpression);
		}

		#endregion

		#region PropertyChanged

		protected void OnPropertyChanged(Expression<Func<object>> propertyExpression)
		{
			var propertyName = propertyExpression.PropertyName();

			OnPropertyChanged(propertyName);
		}

		protected void OnPropertyChanged(string propertyName)
		{
			OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
		}

		protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
		{
			changedActions.ActionOnProperty(e.PropertyName, GetPropertyValue(e.PropertyName));

			dependentProperties.Refresh(e.PropertyName);

			if (IsPropertyChangedSuspended)
			{
				return;
			}

			var handler = PropertyChanged;

			if (handler != null)
			{
				handler(this, e);
			}
		}

		protected void ReactPropertyChangedOnce<TProperty>(Expression<Func<TProperty>> property, Action<TProperty> action)
		{
			changedActions.AddChangeActionOn(property, action, 1);
		}

		protected void ReactPropertyChanged<TProperty>(Expression<Func<TProperty>> property, Action<TProperty> action)
		{
			changedActions.AddChangeActionOn(property, action, 0);
		}
		#endregion

		#region PropertyChanging

		protected void OnPropertyChanging(Expression<Func<object>> propertyExpression)
		{
			var propertyName = propertyExpression.PropertyName();

			OnPropertyChanging(propertyName);
		}

		protected void OnPropertyChanging(string propertyName)
		{
			OnPropertyChanging(new PropertyChangingEventArgs(propertyName));
		}

		protected virtual void OnPropertyChanging(PropertyChangingEventArgs e)
		{
			changingActions.ActionOnProperty(e.PropertyName, GetPropertyValue(e.PropertyName));

			if (IsPropertyChangedSuspended)
			{
				return;
			}

			var handler = PropertyChanging;

			if (handler != null)
			{
				handler(this, e);
			}
		}

		protected void ReactPropertyChangingOnce<TProperty>(Expression<Func<TProperty>> property, Action<TProperty> action)
		{
			changingActions.AddChangeActionOn(property, action, 1);
		}

		protected void ReactPropertyChanging<TProperty>(Expression<Func<TProperty>> property, Action<TProperty> action)
		{
			changingActions.AddChangeActionOn(property, action, 0);
		}
		#endregion

		protected virtual void Dispose(bool disposing)
		{
			if (disposed)
			{
				return;
			}

			if (disposing)
			{
				Release();
			}

			disposed = true;
		}

		protected virtual void Release()
		{
		}

		private void HandleError(ExceptionMessage<object, Exception> obj)
		{
			errorCollection.Add(obj.Exception);
		}

		private string GetError(Func<IEnumerable<ValidationError>> validate)
		{
			var errors = validate().ToList();

			return !errors.Any() ? string.Empty : errors.Select(x => x.Message).JoinMultiLine();
		}
	}
}